﻿#region License Info

//Component of Cronos Package, http://www.codeplex.com/cronos
//Copyright (C) 2009 Anthony Brockwell

//This program is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either version 2
//of the License, or (at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.

//You should have received a copy of the GNU General Public License
//along with this program; if not, write to the Free Software
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Windows.Forms;
using ABMath.ModelFramework.Data;
using ExtraControlLibrary.Forms;

namespace ExtraControlLibrary.DirectedGraphs
{
    public partial class DirectedGraphViewer : UserControl
    {
        #region Delegates

        public delegate void ViewPortChangeNotify(double minX, double minY, double maxX, double maxY);

        #endregion

        private readonly TimeSpan grabTime = new TimeSpan(0, 0, 0, 0, 100);
        private readonly Dictionary<Type, Icon> iconList;
        private List<NodeButton> allButtons;
        private Dictionary<DirectedGraph.NodeInfo, NodeButton> buttonLookup;
        public double ExpandFactor = 1.0;
        private DirectedGraph graph;
        public ViewPortChangeNotify OnViewPortChange;

        private Font textFont;

        public double ViewMaxX = 1;
        public double ViewMaxY = 1;
        public double ViewMinX;
        public double ViewMinY;

        public DirectedGraphViewer()
        {
            MinPixelSize = 58;
            NodeFractionalSize = 0.13;
            SetStyle(
                ControlStyles.AllPaintingInWmPaint |
                ControlStyles.UserPaint |
                ControlStyles.DoubleBuffer, true);
            iconList = new Dictionary<Type, Icon>();
            allButtons = new List<NodeButton>();
            buttonLookup = new Dictionary<DirectedGraph.NodeInfo, NodeButton>();
            InitializeComponent();
        }

        public int MinPixelSize { get; set; }
        public double NodeFractionalSize { get; set; }

        public DirectedGraph Graph
        {
            get { return graph; }
            set
            {
                graph = value;
                SelectedNode = null;
                RebuildAll(true);
            }
        }

        public DirectedGraph.NodeInfo SelectedNode { get; set; }
        protected DirectedGraph.NodeInfo AnchorNode { get; private set; }
        protected int AnchorX { get; set; }
        protected int AnchorY { get; set; }
        protected DateTime AnchorTime { get; set; }
        public event OnNodeConnection NodeConnectEvent;

        public void RebuildAll(bool rebuildButtons)
        {
            if (rebuildButtons)
            {
                toolTip1.Dispose();
                toolTip1 = new ToolTip();

                // remove all existing child buttons
                if (allButtons != null)
                    foreach (NodeButton nb in allButtons)
                        nb.Dispose();

                allButtons = new List<NodeButton>();
                buttonLookup = new Dictionary<DirectedGraph.NodeInfo, NodeButton>();

                if (textFont != null)
                    textFont.Dispose();
                textFont = new Font("Roman", (float) (6.5*ExpandFactor));

                if (Graph == null)
                    return;

                // then add new buttons
                ConstructButtons();
            }
            else
                MoveButtons();
        }

        public void RedrawButtonFor(object item)
        {
            var id = item as IConnectable;
            if (id == null)
                return;
            DirectedGraph.NodeInfo node = graph.GetNodeContaining(id);
            for (int i=0 ; i<allButtons.Count ; ++i)
                if (ReferenceEquals(allButtons[i].NodeInfo, node))
                {
                    // we have a match!
                    allButtons[i].Dispose();
                    var bounds = new Rectangle(0, 0, Size.Width - 1, Size.Height - 1);
                    allButtons[i] = BuildNodeButton(node, bounds);
                    allButtons[i].Show();
                    buttonLookup[node] = allButtons[i];
                }
        }

        private void MoveButtons() // just updates all the coordinates of buttons
        {
            var bounds = new Rectangle(0, 0, Size.Width - 1, Size.Height - 1);

            foreach (DirectedGraph.NodeInfo bi in Graph.Nodes)
            {
                int xp, yp;
                MapToPixels(bi.coordinate, bounds, out xp, out yp);
                NodeButton nb = buttonLookup[bi];
                nb.Hide();
                nb.Left = xp - nb.Width/2;
                nb.Top = yp - nb.Height/2;
            }

            foreach (NodeButton nb in allButtons)
                nb.Show();
        }

        private void ConstructButtons() // constructs all the buttons for a graph
        {
            // Assume coordinate bounds are [0,0] to [1,1]
            var bounds = new Rectangle(0, 0, Size.Width - 1, Size.Height - 1);

            if (buttonLookup.Count != 0)
                throw new ApplicationException("Bad!");
            if (allButtons.Count != 0)
                throw new ApplicationException("Extra Bad!");

            foreach (DirectedGraph.NodeInfo bi in Graph.Nodes)
            {
                NodeButton nb = BuildNodeButton(bi, bounds);
                allButtons.Add(nb);
                if (bi.connectableItem.ToolTipText != null)
                    toolTip1.SetToolTip(nb, bi.connectableItem.ToolTipText);
                buttonLookup.Add(bi, nb);
            }
        }

        private NodeButton BuildNodeButton(DirectedGraph.NodeInfo bi, Rectangle bounds)
        {
            int xp, yp;
            MapToPixels(bi.coordinate, bounds, out xp, out yp);
            int width = (int) (MinPixelSize*ExpandFactor),
                height = (int) Math.Floor(MinPixelSize*0.7*ExpandFactor);
            var nb = new NodeButton(bi)
                         {
                             Left = xp - width/2,
                             Top = yp - height/2,
                             Width = width,
                             Height = height,
                             Parent = this
                         };

            Icon icon = GetIconFor(bi.connectableItem);
            Color clr = bi.connectableItem.GetBackgroundColor();
            nb.BackColor = clr;
            if (icon != null)
            {
                var iconSize = (int) Math.Floor(0.8*height);
                var square = new Bitmap(width, height); // create new bitmap
                Graphics g = Graphics.FromImage(square); // allow drawing to it
                g.Clear(clr);
                g.DrawIcon(icon, new Rectangle(width/2 - iconSize/2, height/2 - iconSize/2, iconSize, iconSize));
                nb.Image = square;
                nb.ImageAlign = ContentAlignment.MiddleCenter;
            }
            else
                nb.Text = bi.connectableItem.GetShortDescription();

            nb.Font = textFont;
            return nb;
        }

        protected virtual void OnNodeConnectEvent(NodeEventArgs nea)
        {
            if (NodeConnectEvent != null)
                NodeConnectEvent(this, nea);
        }

        public event OnNodeConnection NodeSelectEvent;

        public virtual void OnNodeSelectEvent(NodeEventArgs nea)
        {
            if (NodeSelectEvent != null)
                NodeSelectEvent(this, nea);
        }

        private void MapToPixels(double[] coordinate, Rectangle bounds, out int xp, out int yp)
        {
            xp = (int) Math.Round((coordinate[0] - ViewMinX)/(ViewMaxX - ViewMinX)*bounds.Width);
            yp = (int) Math.Round((ViewMaxY - coordinate[1])/(ViewMaxY - ViewMinY)*bounds.Height);
        }

        private double[] MapFromPixels(int[] pixCoordinate, Rectangle bounds)
        {
            var retval = new double[2];
            retval[0] = (pixCoordinate[0] - bounds.Left)/((double) bounds.Width)
                        *(ViewMaxX - ViewMinX) + ViewMinX;
            retval[1] = (-pixCoordinate[1] + bounds.Bottom)/((double) bounds.Height)
                        *(ViewMaxY - ViewMinY) + ViewMinY;
            return retval;
        }

        /// <summary>
        /// icons are associated with specific types of objects in the tree, and are cached
        /// </summary>
        /// <param name="thing"></param>
        /// <returns></returns>
        private Icon GetIconFor(IConnectable thing)
        {
            Icon icon = null;
            if (!iconList.TryGetValue(thing.GetType(), out icon))
                icon = thing.GetIcon();
            return icon;
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            // Call the base class OnPaint
            base.OnPaint(pe);
            Graphics g = pe.Graphics;

            // Assume coordinate bounds are [0,0] to [1,1]
            var bounds = new Rectangle(0, 0, Size.Width - 1, Size.Height - 1);
            g.DrawRectangle(Pens.Black, bounds);

            if (Graph == null)
                return;
            if (Graph.Nodes == null)
                return;

            // Draw links

            var arrowPen = new Pen(Color.Black, 1);
            var ahx = (int) (6*ExpandFactor);
            var ahy = (int) (8*ExpandFactor);
            var aac = new AdjustableArrowCap(ahx, ahy, true);
            CustomLineCap clc = aac;
            arrowPen.StartCap = LineCap.Round;
            arrowPen.CustomEndCap = clc;
            var thickPen = new Pen(Color.Black, 2);

            // Then draw lines and boxes

            foreach (NodeButton nb in allButtons)
            {
                int xp, yp;

                DirectedGraph.NodeInfo nodeInfo = nb.NodeInfo;
                MapToPixels(nodeInfo.coordinate, bounds, out xp, out yp);

                foreach (DirectedGraph.Link x in nodeInfo.outgoingLinks)
                {
                    int xp2, yp2;
                    MapToPixels(Graph.Nodes[x.end].coordinate, bounds, out xp2, out yp2);

                    double dX = (xp2 - xp);
                    double dY = (yp2 - yp);

                    Point p1 = nb.GetEdgeInDirection(dX, dY);
                    Point p2 = buttonLookup[Graph.Nodes[x.end]].GetEdgeInDirection(-dX, -dY);

                    g.DrawLine(arrowPen, p1, p2);
                }
            }

            aac.Dispose();
            arrowPen.Dispose();
            thickPen.Dispose();
        }

        public void SetAnchor(DirectedGraph.NodeInfo ni, int xofs, int yofs)
        {
            if (AnchorNode != ni)
                AnchorNode = ni;
            AnchorTime = DateTime.Now;
            AnchorX = xofs;
            AnchorY = yofs;
        }

        public void EndAnchor(DirectedGraph.NodeInfo baseNode, int xofs, int yofs)
        {
            // can connect
            if (baseNode != null)
            {
                // find the target node
                Control c = GetChildAtPoint(new Point(xofs, yofs));
                var nb = c as NodeButton;
                if (c == null)
                    return;

                DirectedGraph.NodeInfo ni = nb.NodeInfo;
                if (AnchorNode.Equals(ni))
                {
                    var td = new TitleDescriptionDialog(false) { StartPosition = FormStartPosition.CenterParent };
                    td.Title = nb.Text;
                    if (AnchorNode.connectableItem.ToolTipText != null)
                        td.Description = AnchorNode.connectableItem.ToolTipText;
                    if (td.ShowDialog() == DialogResult.OK)
                    {
                        // change the tooltip 
                        AnchorNode.connectableItem.ToolTipText = td.Description;
                        toolTip1.SetToolTip(nb, td.Description);
                    }
                }
                else
                {
                    // connect two nodes
                    MakeConnection(AnchorNode, ni);

                    var nea = new NodeEventArgs();
                    nea.nodeList = new List<DirectedGraph.NodeInfo>();
                    nea.nodeList.Add(AnchorNode);
                    nea.nodeList.Add(ni);
                    OnNodeConnectEvent(nea);
                }
                AnchorNode = null;
            }
        }

        private void DirectedGraphViewer_MouseMove(object sender, MouseEventArgs e)
        {
            // otherwise something is being dragged
            if (AnchorNode != null)
            {
                TimeSpan duration = DateTime.Now - AnchorTime;
                if (duration > grabTime)
                    if ((MouseButtons & MouseButtons.Left) == MouseButtons.Left)
                    {
                        // it's an attempt to move a node
                        double[] newPos = MapFromPixels(new[] {e.X, e.Y}, new Rectangle(0, 0, Size.Width, Size.Height));
                        AnchorNode.coordinate[0] = newPos[0];
                        AnchorNode.coordinate[1] = newPos[1];
                        if (Cursor != Cursors.Hand)
                            Cursor = Cursors.Hand;
                        Refresh();
                    }
            }
        }

        private void LinkedBoxViewer_MouseUp(object sender, MouseEventArgs e)
        {
            switch (e.Button)
            {
                case MouseButtons.Left:
                    // can select, or move, or drop something
                    double[] newPos = MapFromPixels(new[] {e.X, e.Y}, new Rectangle(0, 0, Size.Width, Size.Height));
                    if (Cursor == Cursors.Cross)
                        if (pendingDrop != null) // it's a drop
                        {
                            Graph.AddNode(pendingDrop, new[] {newPos[0], newPos[1]});
                            RebuildAll(true);
                            Cursor = Cursors.Default;
                            pendingDrop = null;
                            Refresh();
                        }
                    break;
            }
        }

        private void MakeConnection(DirectedGraph.NodeInfo n1, DirectedGraph.NodeInfo n2)
        {
            if (n1.Equals(n2))
                return; // don't connect nodes to themselves

            IConnectable c1 = n1.connectableItem;
            IConnectable c2 = n2.connectableItem;

            if (c1.NumOutputs() == 0)
                return;
            if (c2.NumInputs() == 0)
                return;

            // now we create a linksocketchooser form to choose which links to connect
            // the form also will determine the number of possible matching types (if any)
            int idx1 = 0;
            int idx2 = 0;
            var outputs = new List<string>();
            var outputTypes = new List<List<Type>>();
            for (int i = 0; i < c1.NumOutputs(); ++i)
            {
                outputs.Add(c1.GetOutputName(i));
                outputTypes.Add(c1.GetOutputTypesFor(i));
            }
            var inputs = new List<string>();
            var inputTypes = new List<List<Type>>();
            for (int i = 0; i < c2.NumInputs(); ++i)
            {
                inputs.Add(c2.GetInputName(i));
                inputTypes.Add(c2.GetAllowedInputTypesFor(i));
            }
            var lsc = new LinkSocketChooser(outputs, outputTypes, inputs, inputTypes)
                          {StartPosition = FormStartPosition.CenterParent};
            if (lsc.NumPossibilities == 0)
            {
                MessageBox.Show(
                    "These nodes cannot be connected, there are no possible matching input/output sockets.", "Error");
                return;
            }
            if (lsc.NumPossibilities == 1)
            {
                idx1 = lsc.FirstPossibility.source;
                idx2 = lsc.FirstPossibility.sink;
            }
            else if (lsc.ShowDialog(this) == DialogResult.OK)
            {
                idx1 = lsc.SourceIndex;
                idx2 = lsc.SinkIndex;
            }
            else return;

            if (!c2.InputIsFree(idx2))
                graph.RemoveDirectionalLinksTo(c2, idx2);

            var failMsg = new StringBuilder(256);
            if (c2.SetInput(idx2, c1.GetOutput(idx1), failMsg))
            {
                graph.AddDirectionalLink(c1, idx1, c2, idx2);
                Graph.CascadeFrom(n2);
                Refresh();
            }
            else
                MessageBox.Show("Unable to make connection to node.  " + failMsg, "Error");
        }

        public void UpdateFromNodeContaining(object item)
        {
            var id = item as IConnectable;
            if (id == null)
                return;
            DirectedGraph.NodeInfo node = graph.GetNodeContaining(id);
            Graph.CascadeFrom(node);
        }

        private void Zoom(double factor, double[] center)
        {
            // enforce some limits
            if (ExpandFactor*factor < 0.3)
                return;
            if (ExpandFactor*factor > 4)
                return;

            ExpandFactor *= factor;
            double centerX = center[0]; // (ViewMinX + ViewMaxX) / 2.0;
            double centerY = center[1]; // (ViewMinY + ViewMaxY) / 2.0;
            double alphaX = (centerX - ViewMinX)/(ViewMaxX - ViewMinX);
            double alphaY = (centerY - ViewMinY)/(ViewMaxY - ViewMinY);
            double w = ViewMaxX - ViewMinX;
            double h = ViewMaxY - ViewMinY;
            w /= factor;
            h /= factor;
            ViewMinX = centerX - w*alphaX;
            ViewMaxX = centerX + w*(1 - alphaX);
            ViewMinY = centerY - h*alphaY;
            ViewMaxY = centerY + h*(1 - alphaY);

            if (OnViewPortChange != null)
                OnViewPortChange(ViewMinX, ViewMinY, ViewMaxX, ViewMaxY);
        }

        public void DirectedGraphViewer_MouseWheel(object sender, MouseEventArgs e)
        {
            int steps = e.Delta;
            double ef = ExpandFactor;
            // get current mouse position
            Point x = PointToClient(Cursor.Position);
            double[] location = MapFromPixels(new[] {x.X, x.Y}, new Rectangle(0, 0, Width, Height));
            if (steps > 0)
                Zoom(1.25, location);
            if (steps < 0)
                Zoom(0.8, location);
            if (ExpandFactor != ef)
            {
                RebuildAll(true);
                Refresh();
                Focus();
            }
        }

        public void DirectedGraphViewer_KeyDown(object sender, KeyEventArgs e)
        {
            // key pressed
            switch (e.KeyCode)
            {
                case Keys.Delete:
                    // delete the selectednode
                    if (SelectedNode != null)
                    {
                        graph.RemoveNode(SelectedNode);
                        SelectedNode = null;
                        RebuildAll(true);
                        Refresh();
                    }
                    break;

                case Keys.C:
                    // copy the node (without any links)
                    if (SelectedNode != null)
                        if ((e.KeyData & Keys.Control) == Keys.Control)
                        {
                            var form = new BinaryFormatter();
                            var stream = new MemoryStream();
                            form.Serialize(stream, SelectedNode.connectableItem);

                            Clipboard.SetData("DAGNode", stream.ToArray());
                        }
                    break;

                case Keys.V:
                    // paste a node
                    if ((e.KeyData & Keys.Control) == Keys.Control)
                    {
                        var pasteStream = Clipboard.GetData("DAGNode") as byte[];
                        if (pasteStream != null)
                        {
                            var inStream = new MemoryStream(pasteStream);
                            var bf = new BinaryFormatter();
                            var pasted = (IConnectable) bf.Deserialize(inStream);
                            DropNode(pasted);
                        }
                    }
                    break;
            }
        }

        private void DirectedGraphViewer_DragEnter(object sender, DragEventArgs e)
        {
            // hopefully a button is being dragged
            object x = e.Data.GetData(typeof (NodeButton));
            if (x != null)
                e.Effect = DragDropEffects.Move;
        }


        private void DirectedGraphViewer_DragDrop(object sender, DragEventArgs e)
        {
            // hopefully a button is being dragged
            object x = e.Data.GetData(typeof (NodeButton));
            if (x != null)
            {
                var nb = x as NodeButton;
                // get cursor location in current window
                Point p = PointToClient(Cursor.Position);
                if (nb != null)
                {
                    // move the control inside the workspace
                    nb.Hide();
                    nb.Left = p.X - nb.Width/2;
                    nb.Top = p.Y - nb.Height/2;
                    nb.Show();

                    // also correct (x,y) position of nodeinfo
                    double[] newPos = MapFromPixels(new[] {p.X, p.Y}, new Rectangle(0, 0, Size.Width, Size.Height));
                    nb.SetCoordinate(newPos);
                }
                e.Effect = DragDropEffects.Move;
            }
            Refresh(); // so arrows are re-painted
        }

        private void DirectedGraphViewer_Resize(object sender, EventArgs e)
        {
            RebuildAll(true); // buttons have to move
            Refresh();        // so arrows are re-painted
        }

        #region code for dropping nodes

        private object pendingDrop;

        public void DropNode(object item)
        {
            Cursor = Cursors.Cross;
            pendingDrop = item;
        }

        #endregion
    }

    public delegate void OnNodeConnection(object sender, NodeEventArgs e);

    public class NodeEventArgs : EventArgs
    {
        public List<DirectedGraph.NodeInfo> nodeList;
    }
}